Skip to content

feat(generator): DiffTraverser/DiffVisitor for paired tree comparison (C36)#1139

Merged
jsenko merged 16 commits into
mainfrom
c36-diff-traverser
Jul 1, 2026
Merged

feat(generator): DiffTraverser/DiffVisitor for paired tree comparison (C36)#1139
jsenko merged 16 commits into
mainfrom
c36-diff-traverser

Conversation

@jsenko

@jsenko jsenko commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

New infrastructure for paired tree traversal — walks two trees of the same spec version in parallel, dispatching field-level diffs to a pluggable DiffVisitor.

Base classes (visitors/diff/)

  • DiffVisitor — abstract visitor with methods for each field type: diffPrimitive, diffEntity, diffUnion, diffList, diffMap. Strategy selection via getPairingStrategy(propertyName).
  • AbstractDiffTraverser — shared diffMap/diffList/diffEntityField helpers with auto-recursion into matched pairs.
  • CollectionDiff<K,V> — diff result with added/removed/matched entries.
  • PairingStrategy<K,V> — pluggable collection pairing. Built-in: KeyPairingStrategy (maps), IndexPairingStrategy (lists).

Generated per spec version

  • CreateDiffTraversersStage — generates XxxDiffTraverser per spec version (e.g., JD7DiffTraverser), following the same entity-property iteration as CreateTraversersStage.
  • Each entity gets a traverseXxx(original, updated) method that dispatches to the visitor.
  • traverseNode(original, updated) dispatches by instanceof to the correct entity method.

Example generated output

public void traverseFullSchema(JD7FullSchema original, JD7FullSchema updated) {
    if (!visitor.visitEntityPair(original, updated)) return;
    visitor.diffPrimitive("title", original.getTitle(), updated.getTitle());
    this.diffMap("properties", original.getProperties(), updated.getProperties());
    this.diffList("allOf", original.getAllOf(), updated.getAllOf());
    this.diffUnionField("type", original.getType(), updated.getType());
    // ... all fields
}

Context

C36 Phase 1-2 in #1042. Phase 3 (compat checker migration) will be a follow-up.

Test plan

  • All 1129 data-models tests pass
  • Generated code compiles (FullGeneratorTest)
  • Synthetic snapshot tests pass

jsenko added 11 commits June 29, 2026 23:59
…36 Phase 1)

Base classes for paired tree traversal:
- CollectionDiff with Entry/MatchedPair for collection comparison results
- PairingStrategy interface with KeyPairingStrategy (maps) and
  IndexPairingStrategy (lists)
- DiffVisitor abstract base with field-level diff methods and
  strategy selection callback
- AbstractDiffTraverser with shared diffMap/diffList/diffEntityField
  helpers and auto-recursion into matched pairs
New CreateDiffTraversersStage generates per-spec-version diff traversers
that walk two trees in parallel, dispatching to DiffVisitor methods:
- diffPrimitive for primitive fields
- diffEntityField for entity fields (auto-recurses)
- diffUnionField for union fields
- diffMap for map fields (pairs by key, auto-recurses matches)
- diffList for list fields (pairs by index, auto-recurses matches)

Registered in pipeline after CreateTraversersStage.
Base classes registered in LoadBaseClassesStage.
…ategies (C36)

Generated DiffVisitor per spec version with typed, field-specific methods:
- Entity-level: visitFullSchema(original, updated) → boolean
- Primitive: diffFullSchemaTitle(String, String), diffFullSchemaMinLength(Number, Number)
- Entity/Union: diffFullSchemaNot(JsonSchema, JsonSchema)
- Map: diffFullSchemaProperties(CollectionDiff<String, JsonSchema>) +
       visitFullSchemaProperty(String key, JsonSchema, JsonSchema)
- List: diffFullSchemaAllOf(CollectionDiff<Integer, JsonSchema>) +
        visitFullSchemaAllOfItem(Integer index, JsonSchema, JsonSchema)

Method names prefixed with entity name to avoid collisions across entity types.

DiffTraverser now generic (AbstractDiffTraverser<V extends DiffVisitor>),
calls typed visitor methods instead of generic diffPrimitive/diffMap/etc.

Extracted shared pairing logic into PairingStrategy.pairByKey() static method.
KeyPairingStrategy and IndexPairingStrategy both delegate to it.
…ngStrategy (C36)

- MapPairingStrategy<V>.pair(Map, Map) — for map fields
- ListPairingStrategy<V>.pair(List, List) — for list fields, handles
  index conversion internally
- CollectionDiff.pairByKey() — shared static factory for the common
  key-based pairing logic
- DiffVisitor: getMapPairingStrategy/getListPairingStrategy replace
  single getPairingStrategy
…terfaces (C36)

- Delete PairingStrategy marker interface (unused)
- MapPairingStrategy<P, V> — P is the pairing key type (String for default)
- ListPairingStrategy<P, V> — P is the pairing key type (Integer for default)
- Custom strategies can use any P that's meaningful to the visitor
Item visitor methods no longer receive the pairing key since it's
strategy-dependent and not necessarily the map key or list index.
Visitors that need the key context get it from the CollectionDiff
in the collection-level diff method.

Before: visitFullSchema$defs(String key, JsonSchema original, JsonSchema updated)
After:  visitFullSchema$defs(JsonSchema original, JsonSchema updated)
…sitor (C36)

Strategy selection is now a separate concern from comparison logic:
- PairingStrategyProvider interface with getMapStrategy/getListStrategy
- DefaultPairingStrategyProvider: key-based for maps, index-based for lists
- AbstractDiffTraverser takes provider at construction (defaults to DefaultProvider)
- Generated traversers expose two constructors: (visitor) and (visitor, provider)
- DiffVisitor is now a pure base for field-specific comparison methods
…gKey (C36)

- PairingStrategyProvider<P> with P for the pairing key type
- DefaultPairingKey with index (int) and key (String) variants
- DefaultPairingStrategyProvider implements PairingStrategyProvider<DefaultPairingKey>
- KeyPairingStrategy/IndexPairingStrategy produce CollectionDiff<DefaultPairingKey, V>
- AbstractDiffTraverser<P, V> threads P through pairMap/pairList
- Generated traversers use Object for P (erasure-safe, unchecked cast in default constructor)
- Generated visitors use CollectionDiff<Object, V> for collection diff methods
- DiffVisitor is now a pure marker base (strategy selection moved to PairingStrategyProvider)
- Removed PairingStrategy marker interface
…P> (C36)

Both generated classes are now parameterized by the pairing key type P:
- JD6DiffTraverser<P> extends AbstractDiffTraverser<P, JD6DiffVisitor<P>>
- JD6DiffVisitor<P> extends DiffVisitor

CollectionDiff<P, V> flows through the entire stack — no Object erasure
or unchecked casts needed. Usage:
  new JD6DiffTraverser<DefaultPairingKey>(visitor)
…ize item visitors (C36)

- Collection diff methods now receive (original, updated, diff) instead of just (diff)
- Map item visitor methods singularized: visitFullSchemaProperty (not Properties),
  visitFullSchemaDependency (not Dependencies)
- CodeGenContext.singularize made static, reused from both stages
The diff infrastructure uses generics (P type parameter) that cause
JSweet to hang during transpilation. The diff traverser/visitor are
Java-only — not needed in TypeScript.
@jsenko jsenko force-pushed the c36-diff-traverser branch from c41da7e to c571a9d Compare June 30, 2026 21:28
jsenko added 5 commits July 1, 2026 01:47
…y) (C36)

- Remove empty DiffVisitor base class — generated visitors are standalone
- Rename IndexPairingStrategy → DefaultListPairingStrategy
- Rename KeyPairingStrategy → DefaultMapPairingStrategy
- Add traverse(Any, Any) public entry point on AbstractDiffTraverser
  (matches AbstractTraverser pattern, supports union roots like boolean schemas)
- Remove V extends DiffVisitor bound — just V
- Fix unchecked cast in default constructor
…elds (C36)

Generated DiffVisitor now has diff methods for each union type:
- diffJsonSchema(JsonSchema, JsonSchema)
- diffStringStringListUnion(StringStringListUnion, StringStringListUnion)

Generated DiffTraverser now has traverse methods for each union type:
- traverseJsonSchema calls visitor.diffJsonSchema, then auto-recurses
  into traverseFullSchema if both sides are the same entity variant
- Union fields (not, if/then/else, additionalProperties, etc.) now
  call traverseUnionType for auto-recursion instead of just notifying visitor

Handles true → {} (boolean schema to object schema): visitor.diffJsonSchema
gets called with mixed variants, no recursion occurs.
…rser (C36)

Union list/map matched pairs now call traverseUnionType instead of
being silently skipped. E.g., dependencies map pairs call
traverseDependency, allOf list pairs call traverseJsonSchema.

Entity collections still use traverseNode as before.
Generated traverse(Any, Any) now dispatches to both union and entity
traverse methods. Union types are checked first (instanceof) since
entity implementations also implement their union interfaces.

Removed traverseNode from AbstractDiffTraverser — all dispatch is
now in the generated traverse override. Entity field recursion and
collection matched pair recursion call traverse() instead of traverseNode().
Entities that are variants of a union type are already matched by the
union instanceof check. Don't emit redundant entity branches that
would be dead code (e.g., JD7FullSchema is covered by JsonSchema).
@jsenko jsenko merged commit 4a4c034 into main Jul 1, 2026
2 checks passed
@jsenko jsenko deleted the c36-diff-traverser branch July 1, 2026 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant